package com.unitd.frame.sso.common.encrypt;

import com.unitd.frame.comm.utils.encrypt.PKCS7;
import com.unitd.frame.comm.utils.encrypt.SHA1;
import com.unitd.frame.sso.common.config.SSOConfig;
import com.unitd.frame.comm.utils.encrypt.base64.Base64;
import com.unitd.frame.sso.common.parser.JSONParser;
import com.unitd.frame.sso.common.parser.XMLParser;
import com.unitd.frame.comm.utils.ByteGroup;
import com.unitd.frame.comm.utils.RandomUtil;
import com.unitd.frame.comm.exception.EncryptException;
import com.unitd.frame.comm.utils.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.util.Arrays;

/**
 * @desc AES 加解密工具类
 * 说明:采用AES加解密会出现java.security.InvalidKeyException:illegal Key Size异常,该异常出现的原因主要是美国实行软件出口管制造成的.
 * 针对此异常的解决方案为: 在官方网站下载对应的JCE无限制权限策略文件;针对不同的JDK方案有不一样：
 * 1、JDK1.7
 * 下载地址：http://www.oracle.com/technetwork/java/javase/downloads/jce-7-download-432124.html
 * 2、JDK1.8
 * 下载地址：http://www.oracle.com/technetwork/java/javase/downloads/jce8-download-2133166.html
 * 下载后解压,可以看到local_policy.jar和US_export_policy.jar以及readme.txt
 * 如果安装了JRE,将两个jar文件放到%JRE_HOME%\lib\security目录下覆盖原来的文件
 * 如果安装了JDK,将两个jar文件放到%JDK_HOME%\jre\lib\security目录下覆盖原来文件
 * @filename AES.java
 * @copyright www.pactera.com.cn
 * @author Hudan
 * @version 1.0
 * @date 2016/10/27/
 */
public class AES {

	private static final Logger logger = LoggerFactory.getLogger(AES.class.getName());

	private byte[] aesKey;
	private String token;
	private String appId; 							// 微信公众号中对应的ID

	/**
	 * @desc 构造函数
	 * @param token          API 平台上,开发者设置的token
	 * @param encodingAesKey API 平台上,开发者设置的EncodingAESKey
	 * @param appId          API 平台 appid
	 * @throws EncryptException {@link EncryptException}
	 */
	public AES(String token, String encodingAesKey, String appId) throws EncryptException {
		if (encodingAesKey.length() != 43) {
			throw new EncryptException(EncryptException.ERROR_ILLEGAL_AESKEY);
		}
		this.token = token;
		this.appId = appId;
		aesKey = Base64.decode(encodingAesKey + "=");
	}

	/**
	 * @desc 生成4个字节的网络字节序
	 * @param sourceNumber 原始字节数
	 * @return byte[]
	 */
	public byte[] getNetworkBytesOrder(int sourceNumber) {
		byte[] orderBytes = new byte[4];
		orderBytes[3] = (byte) (sourceNumber & 0xFF);
		orderBytes[2] = (byte) (sourceNumber >> 8 & 0xFF);
		orderBytes[1] = (byte) (sourceNumber >> 16 & 0xFF);
		orderBytes[0] = (byte) (sourceNumber >> 24 & 0xFF);
		return orderBytes;
	}

	/**
	 * @desc 还原4个字节的网络字节序
	 * @param orderBytes 网络字节序
	 * @return
	 */
	public int recoverNetworkBytesOrder(byte[] orderBytes) {
		int sourceNumber = 0;
		for (int i = 0; i < 4; i++) {
			sourceNumber <<= 8;
			sourceNumber |= orderBytes[i] & 0xff;
		}
		return sourceNumber;
	}

	/**
	 * @desc 对明文进行加密16位随机数加密混淆
	 * @param text 需要加密的明文
	 * @return 加密后base64编码的字符串
	 * @throws EncryptException {@link EncryptException}
	 */
	public String encrypt(String text) throws EncryptException {
		ByteGroup byteCollector = new ByteGroup();
		byte[] randomStrBytes = RandomUtil.getCharacterAndNumber(16).getBytes(SSOConfig.CHARSET_ENCODING);
		byte[] textBytes = text.getBytes(SSOConfig.CHARSET_ENCODING);
		byte[] networkBytesOrder = getNetworkBytesOrder(textBytes.length);
		byte[] appidBytes = appId.getBytes(SSOConfig.CHARSET_ENCODING);

		/* 16位随机数+4个字节的网络字节序+明文串+ appid(randomStr + networkBytesOrder + text + appid) */
		byteCollector.addBytes(randomStrBytes);
		byteCollector.addBytes(networkBytesOrder);
		byteCollector.addBytes(textBytes);
		byteCollector.addBytes(appidBytes);

		/* pad: 使用自定义的填充方式对明文进行补位填充 */
		byte[] padBytes = PKCS7.encode(byteCollector.size());
		byteCollector.addBytes(padBytes);

		/* 获得最终的字节流, 未加密 */
		byte[] unencrypted = byteCollector.toBytes();

		try {
			/* 设置加密模式为AES的CBC模式 */
			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
			SecretKeySpec keySpec = new SecretKeySpec(aesKey, "AES");
			IvParameterSpec iv = new IvParameterSpec(aesKey, 0, 16);
			cipher.init(Cipher.ENCRYPT_MODE, keySpec, iv);

			/* 加密 */
			byte[] encrypted = cipher.doFinal(unencrypted);

			/* 使用BASE64对加密后的字符串进行编码 */
			return new String(Base64.encode(encrypted));
		} catch (Exception e) {
			e.printStackTrace();
			throw new EncryptException(EncryptException.ERROR_ENCRYPT_AES);
		}
	}

	/**
	 * @desc 对密文进行解密
	 * @param text 需要解密的密文
	 * @return 解密得到的明文
	 * @throws EncryptException {@link EncryptException}
	 */
	public String decrypt(String text) throws EncryptException {
		byte[] original;
		try {
			/* 设置解密模式为AES的CBC模式 */
			Cipher cipher = Cipher.getInstance("AES/CBC/NoPadding");
			SecretKeySpec key_spec = new SecretKeySpec(aesKey, "AES");
			IvParameterSpec iv = new IvParameterSpec(Arrays.copyOfRange(aesKey, 0, 16));
			cipher.init(Cipher.DECRYPT_MODE, key_spec, iv);

			/* 使用BASE64对密文进行解码 */
			byte[] encrypted = Base64.decode(text);

			/* 解密 */
			original = cipher.doFinal(encrypted);
		} catch (Exception e) {
			throw new EncryptException(EncryptException.ERROR_DECRYPT_AES, e);
		}

		String xmlContent, from_appid;
		try {
			/* 去除补位字符 */
			byte[] bytes = PKCS7.decode(original);

			/* 分离16位随机字符串,网络字节序和AppId */
			byte[] networkOrder = Arrays.copyOfRange(bytes, 16, 20);

			int xmlLength = recoverNetworkBytesOrder(networkOrder);

			xmlContent = new String(Arrays.copyOfRange(bytes, 20, 20 + xmlLength), SSOConfig.CHARSET_ENCODING);
			from_appid = new String(Arrays.copyOfRange(bytes, 20 + xmlLength, bytes.length),
					SSOConfig.CHARSET_ENCODING);
		} catch (Exception e) {
			e.printStackTrace();
			throw new EncryptException(EncryptException.ERROR_ILLEGAL_BUFFER);
		}

		/* appid不相同的情况 */
		if (!from_appid.equals(appId)) {
			throw new EncryptException(EncryptException.ERROR_VALIDATE_APPID);
		}
		return xmlContent;
	}

	/**
	 * @desc 将公众平台回复用户的xml消息加密打包.
	 * @param replyMsg 公众平台待回复用户的消息,xml格式的字符串
	 * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
	 * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
	 * @return 加密后的可以直接回复用户的密文
	 * @throws EncryptException
	 */
	public String encryptXMLMsg(String replyMsg, String timeStamp, String nonce) throws EncryptException {
		return encryptMsg(replyMsg, timeStamp, nonce, true);
	}

	/**
	 * @desc 将公众平台回复用户的json消息加密打包.
	 * @param replyMsg 公众平台待回复用户的消息,xml格式的字符串
	 * @param timeStamp 时间戳,可以自己生成,也可以用URL参数的timestamp
	 * @param nonce 随机串,可以自己生成,也可以用URL参数的nonce
	 * @return 加密后的可以直接回复用户的密文
	 * @throws EncryptException
	 */
	public String encryptJSONMsg(String replyMsg, String timeStamp, String nonce) throws EncryptException {
		return encryptMsg(replyMsg, timeStamp, nonce, false);
	}

	/**
	 * @desc 将公众平台回复用户的消息加密打包.
	 * 对要发送的消息进行AES-CBC加密
	 * 生成安全签名
	 * 将消息密文和安全签名打包成xml格式
	 * @param replyMsg    公众平台待回复用户的消息,xml格式的字符串
	 * @param timeStamp   时间戳,可以自己生成,也可以用URL参数的timestamp
	 * @param nonce       随机串,可以自己生成,也可以用URL参数的nonce
	 * @param isXMLParser true XML 解析 ,false JSON 解析
	 * @return 加密后的可以直接回复用户的密文, 包括msg_signature, timestamp, nonce, encrypt的xml格式的字符串
	 * @throws EncryptException {@link EncryptException}
	 */
	private String encryptMsg(String replyMsg, String timeStamp, String nonce, boolean isXMLParser) throws EncryptException {

		/* 随机生成16位字符串混淆加密 */
		String encrypt = encrypt(replyMsg);

		/* 时间戳 */
		if(StringUtils.isBlank(timeStamp)) {
			timeStamp = Long.toString(System.currentTimeMillis());
		}

		/* 生成安全签名 */
		String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
		logger.info("生成安全签名sha1[" + signature + "]");

		/* 判断 XML 还是 JSON 解析 */
		if (isXMLParser) {
			return XMLParser.generate(encrypt, signature, timeStamp, nonce);
		} else {
			return JSONParser.generate(encrypt, signature, timeStamp, nonce);
		}
	}

	/**
	 * @desc 检验消息的真实性,并且获取解密后的明文.
	 * 利用收到的密文生成安全签名,进行签名验证
	 * 若验证通过,则提取xml中的加密消息,对消息进行解密
	 * @param msgSignature 签名串,对应URL参数的msg_signature
	 * @param timeStamp    时间戳,对应URL参数的timestamp
	 * @param nonce        随机串,对应URL参数的nonce
	 * @param encrypt      密文,对应POST请求的数据
	 * @return 解密后的原文
	 * @throws EncryptException {@link EncryptException}
	 */
	public String decryptMsg(String msgSignature, String timeStamp, String nonce, String encrypt)
			throws EncryptException {
		/* 验证安全签名 */
		String signature = SHA1.getSHA1(token, timeStamp, nonce, encrypt);
		logger.info("需要进行解密的签名串sha1[" + msgSignature + "], 验证后的签名串sha1[" + signature + "]");

		/* 与URL中的签名比较是否相等 */
		if (!signature.equals(msgSignature)) {
			throw new EncryptException(EncryptException.ERROR_VALIDATE_SIGNATURE);
		}

		/* 解密 */
		return decrypt(encrypt);
	}

	/**
	 * @desc 验证 URL
	 * @param msgSignature 签名串,对应URL参数的msg_signature
	 * @param timeStamp    时间戳,对应URL参数的timestamp
	 * @param nonce        随机串,对应URL参数的nonce
	 * @param echoStr      随机串,对应URL参数的echostr
	 * @return 解密之后的echostr
	 * @throws EncryptException {@link EncryptException}
	 */
	public String verifyUrl(String msgSignature, String timeStamp, String nonce, String echoStr) throws EncryptException {
		String signature = SHA1.getSHA1(token, timeStamp, nonce, echoStr);
		if (!signature.equals(msgSignature)) {
			throw new EncryptException(EncryptException.ERROR_VALIDATE_SIGNATURE);
		}
		return decrypt(echoStr);
	}
}